package hotciv.standard;

import hotciv.framework.AgingStrategy;
import hotciv.framework.BattleStrategy;
import hotciv.framework.City;
import hotciv.framework.Game;
import hotciv.framework.GameConstants;
import hotciv.framework.GameContext;
import hotciv.framework.GameObserver;
import hotciv.framework.HotCivFactory;
import hotciv.framework.Player;
import hotciv.framework.Position;
import hotciv.framework.Tile;
import hotciv.framework.Unit;
import hotciv.framework.UnitStrategy;
import hotciv.framework.WinningStrategy;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;

/** Skeleton implementation of HotCiv.
 
   From the book "Flexible, Reliable Software Explained"
   To be published 2010 by CRC Press
   Author: 
     Henrik B Christensen 
     Department of Computer Science
     University of Aarhus
   
   This source code is provided AS-IS. It can be
   freely distributed as long as this acknowledgement
   is maintained.
*/

public class GameImpl implements Game, GameContext 
{
	private Player playerInTurn;
	protected Tile[][] world;
	protected Hashtable<Position, Unit> units;
	private Hashtable<Position, City> citys;
	private ArrayList<String> nonMovableAreas;
	private AgingStrategy thisAgingStrategy;
	private WinningStrategy thisWinningStrategy;
	private UnitStrategy thisUnitStrategy; 
	private BattleStrategy thisBattleStrategy;
	private boolean fightJustWon;
	private GameObserver observer;
	
	int[] coordinates= {0,0, -1,0, -1,1, 0,1, 1,1, 1,0, 1,-1, 0,-1,-1,-1};
	HotCivFactory hcv;
	
	public GameImpl(HotCivFactory hcv)
	{
		this.hcv = hcv;
		thisAgingStrategy = hcv.createAgingStrategy();
		thisWinningStrategy = hcv.createWinnningStrategy();
		thisUnitStrategy = hcv.createUnitStrategy();		
		thisBattleStrategy = hcv.createBattleStrategy();
		
		//As standard a null object is initialized to avoid null pointer exceptions
		observer = new NullObserver();
		
		playerInTurn = Player.RED;
		world = new Tile[GameConstants.WORLDSIZE][GameConstants.WORLDSIZE];
		units = new Hashtable<Position, Unit>();
		citys =  new Hashtable<Position, City>();
		nonMovableAreas = new ArrayList<String>();
		fightJustWon=false;
		
		world[1][0] = new StandardTile(GameConstants.OCEANS, 1, 0);
		world[0][1] = new StandardTile(GameConstants.HILLS, 0, 1);
		world[2][2] = new StandardTile(GameConstants.MOUNTAINS, 2, 2);
		
		
		//Sets all non given values in world to plains
		for(int i = 0; i < GameConstants.WORLDSIZE; i++)
		{
			for(int j = 0; j < GameConstants.WORLDSIZE; j++)
			{
				if (world[i][j]==null)
				{
					world[i][j] = new StandardTile(GameConstants.PLAINS, i, j);
				}
			}
		}
		
		units.put(new Position(2,0), new StandardUnit(GameConstants.ARCHER, Player.RED));
		units.put(new Position(3,2), new StandardUnit(GameConstants.LEGION, Player.BLUE));
		units.put(new Position(4,3), new StandardUnit(GameConstants.SETTLER, Player.RED));
		
		citys.put(new Position(1,1), new StandardCity(Player.RED, 1));
		citys.put(new Position(4,1), new StandardCity(Player.BLUE, 1));
		
		nonMovableAreas.add(GameConstants.MOUNTAINS);
		nonMovableAreas.add(GameConstants.OCEANS);
	}
	
	public Tile getTile(Position p) 
	{ 
		return world[p.getRow()][p.getColumn()]; 
	}
	
	public Unit getUnitAt(Position p) 
	{ 
		return units.get(p); 
	}
	
	public City getCityAt(Position p) 
	{ 
		return citys.get(p);
	}
	
	public Player getPlayerInTurn() 
	{ 
		return playerInTurn;
	}
	
	public Player getWinner() 
	{
		return thisWinningStrategy.getWinner(this);
	}
	
	public int getAge() 
	{ 
		return thisAgingStrategy.getAge(); 
	}
	
	/*
	 * This method returns true if the given tile is 
	 * a nonmovable area such as Mountains and Ocens
	 * @param t the given tile to be checked
	 * @return true if the area is a nonmoable area or false otherwise
	 */
	private boolean nonMovableArea(Tile t)
	{
		Position newPos = t.getPosition();
		int col = newPos.getColumn();
		int row = newPos.getRow();
		
		if(nonMovableAreas.contains(t.getTypeString()) 
				|| row > GameConstants.WORLDSIZE || row < 0
				|| col > GameConstants.WORLDSIZE || col < 0)
		{	
			return true;
		}
		
		return false;
	}
	
	private boolean nonMovableArea(Position newPos) {
		int col = newPos.getColumn();
		int row = newPos.getRow();
		
		Tile t = world[row][col];
		
		if(nonMovableAreas.contains(t.getTypeString()) 
				|| row > GameConstants.WORLDSIZE || row < 0
				|| col > GameConstants.WORLDSIZE || col < 0)
		{	
			return true;
		}
		return false;
	}
	
	public boolean moveUnit(Position from, Position to) 
	{
		Tile tile = world[to.getRow()][to.getColumn()];
		Unit u;
		u = units.get(from);
		Boolean legalMove = false;

		//A player are is not allowed to move around with another players units
		if(u.getOwner()!= getPlayerInTurn() ||
				nonMovableArea(tile))
		{
			return  false;
		}
		
		//A Unit cannot stand on another friendly unit
		if(getUnitAt(to) != null)
		{
			if(getUnitAt(to).getOwner() == getPlayerInTurn())
			{
				return false;
			}
		}
	
		if(legalMove(u, from, to))
		{		
		//Units can only move their types fixed distance
			
			if(units.get(to)!=null)
			{
				resolveBattle(from, to, u);
			}	
			else
			{
				units.put(to, u);
			}
			u.decrementMoveCount();
			legalMove = true;
			observer.unitMoved(units.get(to), from, to);
		}
		
		//If units attacks another city they conquer it
		units.remove(from);
		City c = citys.get(to);
		//If a unit moves to a city it's owner now ownes the city!
		if(c != null && legalMove)
		{
			c.setOwner(units.get(to).getOwner());
			observer.cityChanged(c, to);
		}
		
		return legalMove;
	}
	
	/**
	 * 
	 * @param u the unit which the legalmove should be calculated from
	 * @param from the position which the unit is on
	 * @param to the position which the unit should move to if legal
	 * @return true if legal move otherwise false
	 */
	private boolean legalMove(Unit u, Position from, Position to) 
	{
		//Makes sure steps ain't greater than a distance of 1  
		if(u.getMoveCount() > 0)
		{
			if((from.getColumn() - to.getColumn()) > 1 || 
				(to.getColumn() - from.getColumn()) > 1)
			{
				return false;
			}
			
			if((from.getRow() - to.getRow()) > 1 || 
				(to.getRow() - from.getRow()) > 1)
			{
				return false;
			}
			
			return true;
		}
		else
		{
			return false;
		}
	}

	/*
	 * This method iterates trough all cities and check whether it should increment it's production
	 * and if it should make a unit in that city
	 */
	private void AllCitysDoIncrementalTreasuryUnitProduction()
	{
		Enumeration<Position> e = citys.keys();
		while(e.hasMoreElements())
		{
			Position pos = e.nextElement();
			
			if(citys.get(pos).getOwner()==getPlayerInTurn())
			{	
				citys.get(pos).incrementTreasury(6); //All cities produce 6 production
			}
			
			City city = citys.get(pos);
			
			//Units are created in the end of previous players turn.
			//Checks if there a production enough to produce new units.
			if(city.getOwner() != playerInTurn && city.getTreasury() > GameConstants.productionCost(city.getProduction()))
			{
				produceUnit(pos, city);
			}
		}
	}
	
	private void produceUnit(Position pos, City city)
	{	
		int i = 0; int row = 0; int col = 0;
		Position newPos;
		
		while(i < coordinates.length)
		{
			row = pos.getRow() + coordinates[i];
			col = pos.getColumn() + coordinates[i+1];
			
			newPos = new Position(row, col);
			
			//If there ain't room in the city for another unit
			//Can't build on nonMovableAreas
			if(!units.containsKey(newPos) && !nonMovableArea(newPos))
			{
				placeUnitAt(newPos, new StandardUnit(city.getProduction(), getNextPlayerInTurn()));
				city.decrementTreasury(GameConstants.productionCost(city.getProduction()));
				break;
			}
			
			i = i+2;
		}
	} 
	
	private Player getNextPlayerInTurn() {
		switch(playerInTurn)
		{
			case RED: return Player.BLUE; 
			case BLUE: return Player.RED;
			default : return Player.RED;
			
		}
	}

	public void endOfTurn()
	{
		switch(playerInTurn)
		{
			case RED:
			AllCitysDoIncrementalTreasuryUnitProduction();
				playerInTurn = Player.BLUE;
			break;
		
			case BLUE:
			AllCitysDoIncrementalTreasuryUnitProduction();
				playerInTurn = Player.RED;
				thisAgingStrategy.incrementAge();
			break;
		}
		resetMoveCounts();
		observer.turnEnds(playerInTurn, getAge());
	}
	
	/*
	 * This method reset move counts on all units
	 */
	private void resetMoveCounts()
	{
		Enumeration<Unit> e = units.elements();
		while (e.hasMoreElements())
		{
			Unit u = e.nextElement();
			u.resetMoveCount();
		}
	}
	
	public void changeWorkForceFocusInCityAt(Position p, String balance) 
	{
		City c = citys.get(p);
		c.setWorkforceFocus(balance);
		observer.tileFocusChanged(p);
	}
	
	public void changeProductionInCityAt(Position p, String unitType) 
	{
		citys.get(p).setProduction(unitType);
	}
	
	public void performUnitActionAt(Position p)
	{
		thisUnitStrategy.performUnitActionAt(p, this);
	}
	
	public Enumeration<City> getAllCitys()
	{
		return citys.elements();
	}
	
	// TODO Should be specified that there must be a unit at desired position.
	public void addCity(Position p)
	{
		citys.put(p, new StandardCity(getUnitAt(p).getOwner(), 1));
		observer.cityCreated(citys.get(p), p);
	}
	
	public void removeUnitAt(Position p)
	{
		units.remove(p);
	}

	public boolean fightJustWon()
	{
		return fightJustWon;
	}
	
	public boolean battle(GameContext g, Position from, Position to)
	{
		return thisBattleStrategy.getWinningUnit(g, from, to);
	}
	
	public void resolveBattle(Position from, Position to, Unit u)
	{
		//battle(...) returns true if attacking unit are victorious.
		if(battle(this, from, to))
		{
			observer.unitDestroyed(units.get(to), to);
			fightJustWon=true;
			thisWinningStrategy.getWinner(this);
			fightJustWon=false;
			units.put(to, u);
		}
		else 
		{
			observer.unitDestroyed(units.get(from), from);
		}
	}

	@Override
	public void addObserver(GameObserver observer) 
	{
		this.observer = observer;
	}

	protected void placeUnitAt(Position p, Unit u) 
	{
		units.put(p, u);
		observer.unitCreated(u, p);
	}
	
	public void changeTileFocus(Position position)
	{
		observer.tileFocusChanged(position);
	}
}
